在使用 Web Push 推送功能时,很多人会发现:调用

registration.pushManager.subscribe()

之后,浏览器会自动请求网站根目录下的 sw.js 文件,例如:

/sw.js

这并不是 pushManager.subscribe() 本身主动加载了 sw.js,而通常是因为 当前页面还没有注册并控制有效的 Service Worker,浏览器需要先完成 Service Worker 的注册、安装和激活流程,才能创建 Push Subscription。

本文记录这一过程,以及正确的使用方式。


一、什么是 pushManager.subscribe()

pushManager.subscribe() 用于创建一个 Push Subscription,也就是当前浏览器与推送服务之间的订阅关系。

基本代码如下:

const registration = await navigator.serviceWorker.ready;

const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: publicVapidKey
});

console.log(subscription);

订阅成功后,会返回一个 PushSubscription 对象,通常包含:

  • endpoint

  • expirationTime

  • keys.p256dh

  • keys.auth

这些信息需要发送到服务器保存,后续服务器才能根据该订阅向浏览器发送推送消息。


二、为什么会请求根目录下的 sw.js

关键代码通常是:

navigator.serviceWorker.register('/sw.js');

这段代码的作用是注册 Service Worker。浏览器会请求:

https://example.com/sw.js

其中 /sw.js 表示网站根目录下的 sw.js 文件。

完整流程通常如下:

await navigator.serviceWorker.register('/sw.js');

const registration = await navigator.serviceWorker.ready;

const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: publicVapidKey
});

执行过程大致为:

  1. 浏览器请求 /sw.js

  2. 下载并解析 Service Worker 文件

  3. 执行安装阶段

  4. 激活 Service Worker

  5. 等待 Service Worker 准备完成

  6. 调用 pushManager.subscribe()

  7. 创建 Push Subscription

因此,看到浏览器访问 /sw.js 是正常现象。


三、Service Worker 为什么是 Web Push 的前提

Service Worker 是运行在后台的脚本,即使网页没有打开,它也可以接收推送消息。

例如,推送消息到达后,Service Worker 可以监听 push 事件:

self.addEventListener('push', event => {
  const data = event.data
    ? event.data.json()
    : {
        title: '新消息',
        body: '你收到了一条新消息'
      };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png'
    })
  );
});

由于推送消息需要由 Service Worker 在后台接收和处理,所以浏览器必须先注册一个有效的 Service Worker。


四、推荐的注册和订阅代码

页面代码

async function subscribeToPush() {
  if (!('serviceWorker' in navigator)) {
    throw new Error('当前浏览器不支持 Service Worker');
  }

  if (!('PushManager' in window)) {
    throw new Error('当前浏览器不支持 Web Push');
  }

  const permission = await Notification.requestPermission();

  if (permission !== 'granted') {
    throw new Error('用户没有授予通知权限');
  }

  const registration = await navigator.serviceWorker.register('/sw.js');

  await navigator.serviceWorker.ready;

  const subscription =
    await registration.pushManager.getSubscription();

  if (subscription) {
    return subscription;
  }

  const newSubscription =
    await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        '你的 VAPID 公钥'
      )
    });

  return newSubscription;
}

辅助函数:

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);

  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);

  return Uint8Array.from(
    [...rawData].map(char => char.charCodeAt(0))
  );
}

调用:

subscribeToPush()
  .then(subscription => {
    console.log('Push Subscription:', subscription);

    // 将 subscription 发送给服务器保存
  })
  .catch(error => {
    console.error('订阅推送失败:', error);
  });

五、为什么使用 /sw.js 而不是 ./sw.js

以下两种写法看起来相似:

navigator.serviceWorker.register('/sw.js');
navigator.serviceWorker.register('./sw.js');

但它们的作用域可能不同。

使用根路径

navigator.serviceWorker.register('/sw.js');

表示从网站根目录加载 Service Worker:

https://example.com/sw.js

默认作用域是整个站点:

https://example.com/

使用相对路径

如果当前页面地址是:

https://example.com/user/profile.html

执行:

navigator.serviceWorker.register('./sw.js');

浏览器可能会请求:

https://example.com/user/sw.js

默认作用域也可能被限制在 /user/ 目录下。

因此,如果希望 Service Worker 控制整个网站,通常使用:

navigator.serviceWorker.register('/sw.js');

六、sw.js 文件必须放在哪里

如果注册代码是:

navigator.serviceWorker.register('/sw.js');

服务器必须能够正确返回:

https://你的域名/sw.js

例如项目结构可以是:

public/
├── index.html
├── sw.js
└── icons/
    └── icon-192.png

构建后,sw.js 应该被部署到网站根目录。

如果访问:

https://example.com/sw.js

返回的是 404、HTML 页面或错误内容,Service Worker 注册就会失败。


七、sw.js 的基本内容

一个最简单的 Service Worker 可以这样写:

self.addEventListener('install', event => {
  console.log('Service Worker 安装中');
  self.skipWaiting();
});

self.addEventListener('activate', event => {
  console.log('Service Worker 已激活');
  event.waitUntil(self.clients.claim());
});

self.addEventListener('push', event => {
  const data = event.data
    ? event.data.json()
    : {
        title: '通知',
        body: '你收到了一条推送消息'
      };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png'
    })
  );
});

self.addEventListener('notificationclick', event => {
  event.notification.close();

  event.waitUntil(
    clients.openWindow('/')
  );
});

注意:sw.js 中不能直接使用页面脚本里的变量,也不能直接访问 windowdocument

Service Worker 中使用的是:

self

而不是:

window

八、navigator.serviceWorker.ready 的作用

推荐使用:

const registration = await navigator.serviceWorker.ready;

navigator.serviceWorker.ready 会等待,直到当前页面对应的 Service Worker 注册完成并且处于激活状态。

例如:

const registration = await navigator.serviceWorker.ready;

await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: publicVapidKey
});

这样可以避免在 Service Worker 尚未激活时就调用订阅接口。

需要注意,ready 返回的是一个 ServiceWorkerRegistration 对象,而不是 Service Worker 脚本本身。


九、如果已经注册过,是否还会重复访问 sw.js

即使已经注册过 Service Worker,浏览器仍然可能再次检查或请求 sw.js,原因包括:

  • 浏览器定期检查 Service Worker 是否有更新

  • 页面重新打开

  • 调用了 register(),浏览器需要确认脚本是否发生变化

  • HTTP 缓存策略导致重新验证

  • Service Worker 版本发生更新

  • 开发环境中 DevTools 设置了禁用缓存

这并不一定意味着 Service Worker 被重新安装了。

浏览器通常会先检查脚本内容是否变化,只有内容发生变化时,才会触发更新流程。


十、如何避免每次重复注册

可以将注册操作封装起来:

let registrationPromise;

function getServiceWorkerRegistration() {
  if (!registrationPromise) {
    registrationPromise =
      navigator.serviceWorker.register('/sw.js');
  }

  return registrationPromise;
}

使用:

const registration = await getServiceWorkerRegistration();

const subscription =
  await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicVapidKey
  });

不过,即使多次执行 register('/sw.js'),浏览器也不会重复创建多个相同的 Service Worker 注册。浏览器会根据脚本地址和作用域管理已有注册。


十一、优先使用已有的订阅

在调用 subscribe() 之前,建议先调用:

const existingSubscription =
  await registration.pushManager.getSubscription();

完整示例:

async function getPushSubscription() {
  const registration =
    await navigator.serviceWorker.ready;

  let subscription =
    await registration.pushManager.getSubscription();

  if (!subscription) {
    subscription =
      await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: publicVapidKey
      });
  }

  return subscription;
}

这样可以避免重复创建订阅。


十二、常见错误

1. Failed to register a ServiceWorker

可能原因:

  • /sw.js 不存在

  • 返回内容不是 JavaScript

  • 文件路径错误

  • Service Worker 脚本存在语法错误

  • 当前网站不是 HTTPS


2. The operation is insecure

Service Worker 和 Push API 通常要求安全环境:

  • 正式环境必须使用 HTTPS

  • 本地开发时 localhost 通常被浏览器视为安全来源

例如:

https://example.com

通常可以使用。

而:

http://example.com

通常不允许使用。


3. Registration failed - permission denied

可能原因:

  • 用户拒绝了通知权限

  • 浏览器或系统关闭了通知权限

  • 当前页面不是安全来源

  • 浏览器设置禁止通知

代码中应检查:

const permission = await Notification.requestPermission();

if (permission !== 'granted') {
  console.log('通知权限未获得');
}

4. No active Service Worker

说明 Service Worker 可能还没有激活。

可以使用:

const registration = await navigator.serviceWorker.ready;

不要只依赖:

navigator.serviceWorker.register('/sw.js');

因为 register() 返回时,Service Worker 不一定已经处于激活状态。


5. userVisibleOnly 错误

Chrome 等浏览器通常要求:

userVisibleOnly: true

这表示每一次推送消息都必须向用户展示可见通知。

正确写法:

registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: publicVapidKey
});

十三、如何排查 /sw.js 请求问题

可以打开浏览器开发者工具:

  1. 打开 DevTools

  2. 进入 Application

  3. 找到 Service Workers

  4. 查看当前注册状态

  5. 查看注册的脚本地址和作用域

  6. 点击 Unregister 取消注册

  7. 勾选 Update on reload 方便调试

同时在 Network 面板中检查:

/sw.js

重点查看:

  • HTTP 状态码是否为 200

  • Content-Type 是否正确

  • 返回内容是否真的是 Service Worker JavaScript

  • 是否被重定向到登录页

  • 是否被前端路由返回了 index.html

  • 是否存在缓存问题


十四、需要注意的关键点

1. Service Worker 文件必须使用正确的 MIME 类型

服务器最好返回:

Content-Type: application/javascript

或者:

Content-Type: text/javascript

不要把 sw.js 返回成 HTML。


2. Service Worker 作用域受脚本路径影响

例如:

navigator.serviceWorker.register('/assets/sw.js');

默认可能只能控制 /assets/ 目录。

如果希望控制根目录,需要:

navigator.serviceWorker.register('/sw.js');

或者使用服务器响应头扩大作用域:

Service-Worker-Allowed: /

但通常把 sw.js 放在网站根目录是最简单的方式。


3. Push 订阅不是永久不变的

Push Subscription 可能因为以下原因失效:

  • 用户清除浏览器数据

  • 用户取消网站权限

  • 浏览器更换推送服务

  • 订阅过期

  • 浏览器或系统策略变化

因此服务器发送推送失败时,应考虑删除旧订阅,并允许客户端重新订阅。


十五、完整的推荐流程

async function initPush() {
  if (!('serviceWorker' in navigator)) {
    throw new Error('不支持 Service Worker');
  }

  if (!('PushManager' in window)) {
    throw new Error('不支持 Push API');
  }

  const permission =
    await Notification.requestPermission();

  if (permission !== 'granted') {
    throw new Error('通知权限未授权');
  }

  await navigator.serviceWorker.register('/sw.js');

  const registration =
    await navigator.serviceWorker.ready;

  let subscription =
    await registration.pushManager.getSubscription();

  if (!subscription) {
    subscription =
      await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          publicVapidKey
        )
      });
  }

  console.log('当前 Push Subscription:', subscription);

  // 发送到服务器
  await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(subscription)
  });

  return subscription;
}

十六、结论

调用:

registration.pushManager.subscribe()

后访问根目录下的:

/sw.js

通常是正常现象。

根本原因是:

  1. Web Push 必须依赖 Service Worker

  2. Service Worker 通常通过 /sw.js 注册

  3. 浏览器需要确认 Service Worker 已安装并激活

  4. 浏览器可能定期重新检查 sw.js 是否更新

推荐使用以下方式:

await navigator.serviceWorker.register('/sw.js');

const registration =
  await navigator.serviceWorker.ready;

const subscription =
  await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicVapidKey
  });

只要 /sw.js 能够正常访问、页面使用 HTTPS、通知权限已授权,并且 Service Worker 已经成功激活,这种自动请求一般不需要额外处理。